lazy load就是只載入精簡的核心,其他的程式到需要時才動態載入。這個主題其實上http://ajaxpatterns.org網站,找一下On-Demand Javascript就可以看到詳細的說明以及許多的解決方法。所以我的就做做參考吧...
要動態載入外部的Javascript檔案,一般有兩種作法,一種使用DOM一種使用ajax。兩種方法各有優缺點,所以要使用哪個方法就看需要囉。
使用ajax + eval
使用ajax的方式,就是用ajax把Javascript檔案內容以字串的方式取得,然後透過eval()函數來執行。
以下是一個簡單的例子(loader.js,為了行文方便,我都是取loader.js中部份程式,但是本文中的方法可以組成一個完整的程式來用):
(function(){
var window = this;
var document = window.document;
var _xmlhttp = function() {
var ajax = function() {
return new XMLHttpRequest();
}();
return function(){return ajax;};
}();
$ = {};
$.read = function (path) {
try {
var loader = _xmlhttp();
loader.open("GET", path, false);
loader.send(null);
if (loader.readyState == 4) {
if (loader.status == 200) {
return loader.responseText;
}
} else {
throw "ERROR: " + loader.status;
}
} catch (e) {
alert(e);
}
};
$.require = function (path, callbacks) {
try {
if (window.execScript) {//for IE
window.execScript(this.read(path));
} else {
window.eval.call(window, this.read(path));
}
} catch(e) {
alert(e);
}
if (callbacks) {
callbacks.call(window);
}
};
})();
(為了偷懶,我沒有用非同步的方式跑ajax,如果要用非同步的方式跑,那需要用onreadystatechange事件來處理)
loader.js會加入一個函數叫做$.require,用來動態載入Javascript。第一個參數Javascript檔案的路徑,第二個則是要同時執行的程式(必須是函數)。使用eval有一個特殊的限制,就是他的execution context是由執行時的execution context決定的,但是載入的Javascript應該都會在Global執行,所以做了特別處理。另外,IE的eval()函數無法這樣改,所以只好用他的execScript函數來做。
接下來是網頁:
<script src="js/loader.js"></script>
<div id="target"></div>
<script>
$.require(
'iron016.js',
function(a, b){
return function(){
append(a, b);
};
}('target', 'it changed.')
);
</script>
<input type="button" value="test" onclick="append('target','it should be.')">
最後是動態載入的程式碼(iron016.js):
function append(id, content){
document.getElementById(id).innerHTML = content;
}
使用ajax來動態載入Javascript有一個特殊限制,就是ajax需要遵守same origin規則,所以程式只能來自同一個domain。如果想要動態載入在不同domain上的javascript,那就只好使用下一節介紹的DOM載入方式。
使用DOM
使用dom的方式,簡單地說就是在DOM中間加入一個script element node,src設為你要加入的script路徑。使用DOM的方式動態加入Javascript會碰到一個問題,就是把這個script element node附加到DOM tree的操作完成時,瀏覽器還需要去伺服器把script下載完畢才能用,這樣會有時間差,如果接下來要直接利用到動態加入的程式碼就可能發生錯誤。
解決方式是把要立刻執行的程式傳給負責動態載入的程式,然後把這個要執行的程式改到script element node的onload或onreadystatechange事件處理函數中執行。IE的onload事件要在文件載入時才會觸發,所以必須使用onreadystatechange事件來處理。以下是一個簡單的例子:
首先是用來做動態載入的程式碼(loader.js):
(function(){
var window = this;
var document = window.document;
var head = document.getElementsByTagName('head')[0];
if (!head) {
head = document.createElement('head');
document.ownerDocument.insertBefore('body', head);
}
$ = {};
$.include = function(path, callbacks) {
var node = document.createElement('script');
node.type = "text/javascript";
node.src = path;
head.appendChild(node);
if (node.attachEvent) {//it's IE
var f = function() {
if (node.readyState == 'loaded') {
if (callbacks) {
callbacks.call(window);
node.detachEvent('onreadystatechange',f);
}
head.removeChild(node);
delete node;
delete callbacks;
}
};
node.attachEvent('onreadystatechange', f);
} else {
var f = function() {
if (callbacks) {
callbacks.call(window);
node.removeEventListener('load',f,false);
}
head.removeChild(node);
delete node;
delete callbacks
};
node.addEventListener('load',f,false);
}
};
})();
網頁中要把載入的方法改成$.include,動態載入的程式碼(iron016.js)則與上例相同。
loader.js會加入一個$.include函數來動態載入其他script,第一個參數是script path,第二個參數是要接著執行的程式,在上例中必須是一個函數。用DOM的方式做載入,就不會有相同domain的限制,使用起來也很方便。
script element node在載入完成後,javascript程式就會加到javascript的環境中,所以把script element node刪除也不會影響程式的執行。
重複載入
如果重複載入之前已載入的Javascript,其實是在浪費時間,要避免重複載入,可以用一個簡單的方法來做管理,就是在載入時做一個簡單的確認。所以我在loader.js裡面加上一段程式:
Array.prototype.find = function(v) {
var i = 0;
for (;i < this.length; i++) {
if (this[i] ==v) {
return true;
}
}
return false;
};
var loaded = [];
然後在動態載入的函數開頭,加入判斷:
$.include = function(path, callbacks) {
if (!loaded.find(path)) {
loaded.push(path);
....
} else {
if (callbacks) {
callbacks.call(window);
}
}
};
另外一個函數也做相同的處理:
$.require = function(path, callbacks) {
if (!loaded.find(path)) {
loaded.push(path);
....
} else {
if (callbacks) {
callbacks.call(window);
}
}
};
已載入的程式,就會把路徑加入loaded變數,這樣就可以用這個變數來判斷是否有動態載入過這個javascript。
管理dependency
如果動態載入的東西更龐大,那就可能會碰到dependency的問題,光是使用之前的方式還會有所不足,這個時候就必須要用dependency管理的方式來解決。
常見的方法是在套件中宣告每個要載入程式的先備條件,也就是需要先載入哪些程式。例如載入c.js之前,必須先載入a.js跟b.js,那可以這樣子定義:
var deps = {
'c.js': ['a.js', 'b.js']
};
取出要先預先載入的程式後,就可以先透過前面說的loaded變數過濾,然後再這些函數動態載入,最後處理真正要載入的程式。
補充一下,因為關鍵字new ActiveXObject似乎會被擋掉,所以我把產生ajax物件的範例程式刪掉一大塊使用MSXML的部份。